菜单路由跳转:菜单事件传递
事件传递的设计原则
基础组件(如 Menu)的事件处理遵循一个核心原则:组件本身只负责传递事件,不处理业务逻辑。具体的业务逻辑(如路由跳转)应该在引用该组件的页面或布局中编写。对于 Menu 组件来说,事件传递的路径是:
Menu 组件 (emit) -> DefaultLayout (handleSelect -> router.push)
text
这种分层设计让 Menu 组件保持纯粹的 UI 职责,而路由导航等业务逻辑集中在 Layout 层管理。
类型定义的抽离
Menu 组件内部使用的类型需要抽离到独立的类型文件中,方便在 Layout、路由守卫等其他位置复用:
// types/menu.ts
export interface AppRouteMenuItem {
path: string
name: string
meta?: {
title?: string
key?: string
icon?: string
hidden?: boolean
order?: number
}
children?: AppRouteMenuItem[]
}
// Menu 组件 emit 的事件类型
export type EmitSelectType = AppRouteMenuItem
typescript
从 select 事件获取完整的菜单项数据
Element Plus 的 el-menu 组件在触发 select 事件时,回调参数是 index(字符串)和 indexPath(路径数组)。但对我们来说,仅仅拿到 index 值是不够的,我们需要获取完整的菜单项数据(包含 path、name、meta 等)。
递归查找菜单项
在 useMenu composable 中实现一个递归查找方法 getItem,根据 index 值在菜单树中查找对应的菜单项:
// composables/useMenu.ts
function getItem(
menus: AppRouteMenuItem[],
index: string
): AppRouteMenuItem | undefined {
for (let i = 0; i < menus.length; i++) {
if (menus[i].meta?.key === index) {
return menus[i]
}
if (menus[i].children && Array.isArray(menus[i].children)) {
const item = getItem(menus[i].children!, index)
if (item) return item
}
}
return undefined
}
typescript
Menu 组件中的事件处理
在 Menu 组件的 select 事件中,使用 getItem 获取完整的菜单项数据后 emit 出去:
<!-- Menu.vue -->
<script setup lang="ts">
import { getItem } from './useMenu'
const emit = defineEmits<{
select: [item: AppRouteMenuItem]
}>()
const handleSelect = (index: string) => {
const item = getItem(filteredMenu.value, index)
if (item) {
emit('select', item)
}
}
</script>
vue
注意这里加了 if (item) 的空值判断,因为 getItem 可能返回 undefined(菜单项未找到的情况)。
在 Layout 中实现路由跳转
DefaultLayout 是 Menu 组件的消费者,在这里接收 select 事件并执行路由跳转:
<!-- layouts/DefaultLayout.vue -->
<script setup lang="ts">
import type { AppRouteMenuItem } from '~/types/menu'
const router = useRouter()
const handleSelect = (item: AppRouteMenuItem) => {
if (item && item.name) {
// 使用路由的 name 属性进行跳转
// name 是由文件路由自动生成的,对应相对路径
router.push({ name: item.name })
}
}
</script>
<template>
<el-menu @select="handleSelect">
<!-- menu items -->
</el-menu>
</template>
vue
path 与 name 的区别
在路由跳转时有两种选择,它们的区别值得理解:
| 属性 | 来源 | 格式 | 使用方式 |
|---|---|---|---|
item.path | 路由配置中的 path 字段 | 绝对路径,如 /components/icon/ep-icon-picker | router.push(item.path) |
item.name | 文件路由自动生成 | 拼接的完整路径名,如 components-icon-ep-icon-picker | router.push({ name: item.name }) |
对于多级嵌套路由,使用 name 进行跳转更可靠,因为它由路由系统自动生成,不受路径拼接错误的影响。
Props 属性排列规范
在模板中为组件绑定属性时,推荐的排列顺序是:
- 固定 Props 在最前面(如
default-active、mode) - 动态/条件 Props 在中间(如
:collapse、background-color) - 事件绑定 在最后面(如
@select、@open)
<el-menu
default-active="1"
mode="vertical"
:collapse="isCollapsed"
:background-color="bgColor"
@select="handleSelect"
>
vue
这种排列习惯让模板的可读性更好,固定属性、动态属性和事件三个层次一目了然。
关键要点总结
- 基础组件只传递事件,不处理业务逻辑,路由跳转等操作应在引用该组件的 Layout 中实现
- 递归查找方法
getItem是在菜单树中根据 key 精确定位菜单项的核心工具 - 使用
router.push({ name: item.name })进行路由跳转比直接拼接 path 更可靠,尤其对于多级嵌套路由 - Props 排列遵循固定/动态/事件的顺序,是保持模板可读性的好习惯
↑